Skip to content

Keep DocumentDB Local state in sync, explain failures, and unify tree progress - #876

Merged
Tomasz Naumowicz (tnaum-ms) merged 36 commits into
release/0.10.0from
dev/tnaum/quickstart-improvements
Aug 10, 2026
Merged

Keep DocumentDB Local state in sync, explain failures, and unify tree progress#876
Tomasz Naumowicz (tnaum-ms) merged 36 commits into
release/0.10.0from
dev/tnaum/quickstart-improvements

Conversation

@tnaum-ms

@tnaum-ms Tomasz Naumowicz (tnaum-ms) commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes #873

Problem

Stopping DocumentDB Local outside VS Code left the extension believing it was still running, and the resulting connection failure surfaced as a raw driver error (ECONNREFUSED, a server selection timeout) that said nothing about the container. The reporter asked for two things: validate the container on connect, and recover from, or at least explain, the failure afterwards.

The first half was straightforward. The second half turned out to be a general problem: a connection can fail for reasons that have nothing to do with the database, and we had no way for the component that owns that infrastructure to say so.

What this does

Quick Start state accuracy

  • Quick Start reconciliation is demand-driven, on first expansion or direct use, instead of running at extension activation.
  • The Quick Start root refresh performs an authoritative durable-store and Docker reconciliation, with tracing and retry-safe failure handling.
  • Running, stopped, missing and credentials-missing states stay accurate while avoiding redundant background probes.
  • Expanding the cluster preflights the container, so stale Running state can never reach the database client. A stopped instance offers to start.
  • Managed-instance tooltips carry container and Docker host details, including the conventional 12-character container ID.

Error translation (ConnectionDiagnosticsService)

A registry where the source that owns the infrastructure explains a failure in its own words. Providers ship for DocumentDB Local (Docker), Kubernetes port forwarding, and Atlas TLS rejections.

It is deliberately translation only. Providers return text; they never show UI, never recover, never retry, and never touch the error object. That last constraint is load-bearing: a lot of code here inspects errors by identity rather than text, and all of it would break silently otherwise.

Why the error object is never modified
  • instanceof UserCancelledError decides failure versus cancellation, in roughly 25 places
  • instanceof QueryError, MongoBulkWriteError and SettingsHintError change how a failure is handled
  • error.code carries server codes (115, 235) and socket codes (ECONNRESET, ENOTFOUND)
  • errorCodeExtractor.ts reads error.cause.cause.code at a fixed depth, so an extra wrapper level breaks Collection view error-code detection
  • extractErrorCode() parses a [CODE-12345] prefix from the start of a message, so prepending text breaks the shell and the playground
  • the tRPC boundary rebuilds errors as { code, name, message, stack, cause }, so a custom property never reaches a webview anyway

Covered surfaces. Cluster connect and list databases in all views; everything below a cluster in all four tree views, via one catch in BaseExtendedTreeDataProvider; the Collection view query and Query Insights; the Document view; tree-node commands; the shell; the query playground. Background paths that show nothing (count badges, prefetches) are deliberately left alone.

Webviews ask through a single shared procedure, common.explainOperationFailure, since an explanation cannot ride along on an error across the tRPC boundary.

Shell terminal no longer disappears on a failed connect

The connect-failure path wrote an error line and then immediately fired the close emitter, which disposes the terminal and takes the message with it. There is no VS Code setting for this: for an extension-owned Pseudoterminal, onDidClose always disposes.

It now shows a prompt instead. ShellSessionManager.evaluate() re-runs initialize() while the session is uninitialized, which a failed connect leaves it as, so the next command the user types becomes the retry: start the container, press Enter, you are connected. The failure is also reported through two channels that outlive the terminal, a notification and an outputChannel.error line carrying the raw driver message plus the provider id and explanation, so a shared output channel is enough to diagnose a report remotely.

Message style

Messages avoid asserting what happened, since we cannot know. "We cannot find the DocumentDB Local container. It was very likely removed outside VS Code. You can recreate it from the Connections view, which reuses the existing data volume."

Documentation

.github/skills/error-translation/SKILL.md covers the one rule (providers translate, never show UI), the three ways a provider identifies its own clusters, why verdicts must not be cached, the identity checks that forbid touching errors, which background paths to leave alone, and the message conventions.

Follow-up

#877 captures ideas for where this could go next (recovery buttons, self healing, proactive detection), explicitly as ideas needing discussion rather than a plan. It is labelled on-hold and needs-triage.

Not included here: an Azure diagnostics provider, so Azure firewall and IP-rule rejections still surface raw. Noted in #877.

Review follow-ups

A review pass over the branch turned up a set of defects. Each is fixed here in its own commit.

Two were user-visible regressions.

Making hydration lazy also made it fatal. listByLabel rejects when there is no docker binary or
the daemon is stopped, and both Quick Start entry points awaited ensureHydrated() unguarded, so
the tree row rendered empty behind an error toast and the webview, the one surface that can diagnose
Docker, never opened. Both call sites now tolerate the rejection; the service stays retryable.

registerCommandWithTreeNodeUnwrappingAndModalErrors asked for an explanation on every error that
was not a UserFacingError, and a provider is allowed to answer without inspecting the error at
all. Escaping a wizard on a stopped instance therefore raised a modal saying DocumentDB Local was
not running. explain() now returns undefined for a UserCancelledError before any provider is
asked.

The rest

  • inspectContainer reports "could not ask" and "not there" identically, so stopping Docker Desktop
    made the preflight assert the container had been removed outside VS Code and recommend recreating
    it. The preflight confirms the daemon before concluding missing, and a new dockerUnreachable
    verdict carries wording that matches what actually happened.
  • prepareForConnection() corrected state, fired the status emitter and could warn, so the
    error-translation provider was repairing state and repainting the tree while claiming to only
    translate. The verdict is split out as a read-only inspectManagedInstance().
  • A diagnosis becomes the heading of a modal, where the four-line bulleted Atlas text rendered as a
    block of bold lines. The provider now returns a single-paragraph summary; the Discovery-view modal
    keeps the long form where it belongs, in the detail area.
  • MessageOptions.detail is only rendered for modal messages, so the tree base class was
    suppressing the default notification and then hiding the driver text entirely. It is appended to
    the message instead, matching displayErrorMessage.
  • The shell's connect-failure line goes to an output channel users are encouraged to share, and a
    driver error can quote the connection string, which for a Quick Start instance carries a generated
    password. Cached secrets are redacted first.
  • Expanding the managed cluster raised a modal that blocked the expansion until it was answered and
    then left the node empty, and every other non-ready verdict rendered as silence. Each verdict now
    gets an actionable row, which also removes a module-level prompt singleton that would have been
    shared across aliases.
  • The status events reconciliation fires re-enter getChildren() once hydration has completed,
    and the background-probe cooldown was still unarmed, so the very first expansion started a
    redundant docker inspect and flashed Refreshing… on the row. Hydration now arms the cooldown,
    as refreshHydratedState() already did.
  • The explain() deadline was per provider, so three registered sources could hold an error back
    for 15s on top of the driver's own timeout. It is now one budget for the whole call.
  • Smaller: unwrapArgs moved back inside the guarded block, display labels instead of raw
    identifiers (NotInstalled, unixSocket) in the tooltip, narrower markdown escaping, and
    progress during the explicit deep refresh.

Two findings were left alone on purpose. Collapsing the Quick Start root is what makes hydration
lazy, and the Quick Start provider ignoring the error shape is the premise that lets it answer at
all; both are recorded with their reasoning rather than silently dropped.

The review and its resolution live in docs/ai-and-plans/PRs/876-quickstart-error-translation-review.md.

One way to show progress in the tree

Quick Start drew its own loading~spin icons and a · Refreshing… text hint instead of the tree
framework's node progress (ext.state), so one view expressed the same idea two ways. Three of
those sites were also plain gaps: starting or stopping from the tree gave no feedback at all until
the service pushed a new state, and the node's own refresh spun the view title rather than the node.

The obstacle was that the work is service-owned. A start can come from the Quick Start webview, a
lifecycle command, or the background freshness probe, so the row had nothing to await.
QuickStartService now publishes an awaitable handle for in-flight work (getInFlightOperation)
alongside a dedicated onDidChangeOperation event, kept separate from onDidChangeStatus because
that one's listeners rebuild the whole Connections view. A small bridge maps the handle onto
ext.state.runWithTemporaryDescription for the instance row.

  • The busy child row, the Starting/Stopping spinners and the · Refreshing… hint are gone. The row
    is decorated the way a deleting collection or an importing database already is.
  • Transitional rows keep their state_* context values. The framework overlays description and
    icon but never contextValue, which the lifecycle menus gate on.
  • The bridge is deferred through a microtask: work can start from inside a tree render, and applying
    state fires a refresh synchronously.
  • The lifecycle commands are deliberately not wrapped as well. The bridge already covers every
    origin, and two owners of one indicator would clip it early.
  • Provisioning keeps its own spinner. It is also reachable by observation, from a durable lease
    written by another window or a previous session, so there is no promise to await and no instance
    row to attach to yet. The call site records that reasoning.

Transitional rows and the overlay now render identical strings, with the host dropped from both: it
is not actionable mid-transition, and two copies of one sentence can only drift apart. Where a host
does belong, as in Running · localhost:10260, it is composed outside the localized string so the
separator is not something a translation can reorder, and the state half comes from the same
function that builds the tooltip.

New tests cover the operation handle in the service and the bridge itself: the correct row and
label per operation kind, no stacking while one operation runs, pickup of the next operation, and
silence when nothing is in flight.

Cluster commands on the managed instance

The running instance row looked like a cluster but carried none of the cluster commands, so New
Database, Launch Shell and Refresh were missing, and Copy Connection String had to exist twice.
That is UX review item 20, from #790.

The row deliberately still does not take treeItem_documentdbcluster. That tag gates thirteen
entries, six of which resolve the node through connection storage — rename, move, remove, update
credentials, update connection string, plus Azure data migration — and a service-owned instance has
no storage record for them to act on. Granting the tag and excluding the six would mean every
future cluster command reaches this row by default, which is the wrong default for a node that is
not a stored connection.

Commands are opted in individually instead, gated to state_running because every other state
renders a plain row with no cluster to dereference. The contribution test pins both directions:
the three that must appear, the six that must not, and a guard that the tree item never starts
using CLUSTER_ITEM_CONTEXT_VALUE.

Copy Connection String and Copy Password stay as Quick Start commands rather than folding into the
generic ones: they are also offered while the instance is stopped, where the row is not a
cluster item at all and the generic command would have no getCredentials() to call.

Second review pass

A later review found four more defects. All four were real, and three were surfaces this description claimed but the code had not reached. Each is fixed in 0194ac26.

  • A Docker outage still read as a deleted container. The preflight learned to tell "could not ask" from "not there"; the background freshness probe kept the old logic and set missing on any empty inspect. Stopping Docker Desktop under an expanded node offered to recreate a container that was sitting on disk. refreshLiveState() now confirms the daemon is answering first, and keeps the last known state otherwise.
  • The database commands were never wired up. Only connection-management commands used the diagnostics-aware wrapper. Reusing the modal wrapper would have promoted every UserFacingError from nine commands into a blocking dialog, so there is a new registerCommandWithTreeNodeUnwrappingAndDiagnostics instead: same translation, reported the way those commands already report failures.
  • The explain deadline did not stop anything. Promise.race abandons the losing promise rather than cancelling it, so later providers were still queried and a late answer still recorded connectionDiagnostics.explained for a caller that had already been handed undefined. The deadline is now an AbortSignal checked before each provider and again after each await.
  • The shell was only half covered. A session that connects fine can still break underneath the user; that path printed the raw driver error. handleEvalError now asks for a diagnosis too, keeping the SettingsHintError hint and the error-code-50 tip intact. It became async as a result, so a failed command can wait on a diagnosis before the prompt returns — bounded by the 5s deadline.

Also from the first review: the tooltip's product labels were raw strings while the webview localizes the same words, and 'Dev Container' differed from the webview's 'Dev container' by one capital letter, which extracted as a second key. Both fixed; net zero new strings.

Validation

  • npm run l10n
  • npm run prettier-fix
  • npm run lint — clean
  • npx jest --no-coverage — 218 suites, 3,499 tests
  • npm run build — clean

New tests cover the service (first answer wins, throwing and stalling providers are skipped, the error comes back untouched), each of the three providers, and the tree base class translation including a regression guard that background count paths never invoke it.

Copilot AI lite review requested due to automatic review settings August 7, 2026 12:55
@tnaum-ms
Tomasz Naumowicz (tnaum-ms) requested a review from a team as a code owner August 7, 2026 12:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors Local Quick Start state management so Docker/durable-store reconciliation becomes demand-driven (on first tree expansion or opening the webview) rather than running at extension activation, and it enriches the Connections tree presentation with more detailed tooltips and an explicit deep-refresh action.

Changes:

  • Move Docker readiness checks behind QuickStartService.checkDockerReadiness() and introduce lazy hydration (ensureHydrated) + explicit deep refresh (refreshHydratedState).
  • Update the Connections tree Quick Start node to start collapsed, hydrate on-demand, and add richer managed-instance tooltips (container + Docker host details).
  • Add command contribution/tests for a single deep Refresh entry on the Quick Start root node and expand Jest coverage for the new hydration/refresh behaviors.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/webviews/documentdb/localQuickStart/localQuickStartRouter.ts Switch Docker readiness probing from ContainerRuntime to QuickStartService.checkDockerReadiness.
src/webviews/documentdb/localQuickStart/localQuickStartRouter.test.ts Update router mocks to cover the new Docker readiness entry point.
src/tree/connections-view/LocalQuickStart/revealQuickStartInstance.test.ts Stabilize reveal tests by mocking lazy-hydration state.
src/tree/connections-view/LocalQuickStart/LocalQuickStartItem.ts Implement lazy hydration on expansion, add deep refresh, and build enriched managed-instance tooltips.
src/tree/connections-view/LocalQuickStart/LocalQuickStartItem.test.ts Add tests validating “collapsed + no Docker work until expansion” and hydration/probe behavior.
src/tree/connections-view/LocalQuickStart/LocalQuickStartItem.credentials.test.ts Add tooltip assertions for retained Docker/container facts and 12-char container ID.
src/services/localQuickStart/QuickStartService.ts Add tracing, cached Docker readiness snapshotting, lazy hydration, and shared reconciliation.
src/services/localQuickStart/QuickStartService.test.ts Add tests for readiness retention, hydration concurrency/sharing, retryability, and refresh interactions.
src/documentdb/ClustersExtension.ts Remove eager Quick Start reconciliation during activation; rely on lazy reconciliation.
src/commands/localQuickStart/openLocalQuickStart.ts Await authoritative hydration before revealing the Quick Start webview.
src/commands/localQuickStart/openLocalQuickStart.test.ts New test ensuring webview reveal waits for hydration.
src/commands/localQuickStart/contributions.test.ts Validate the deep Refresh context menu appears exactly once on the Quick Start root.
package.json Contribute a context-menu Refresh entry for the Quick Start root node.
l10n/bundle.l10n.json Add localized strings used by the new tooltip/detail labels.
Suppressed comments (1)

src/tree/connections-view/LocalQuickStart/LocalQuickStartItem.ts:74

  • Execution-target labels in the tooltip should reuse existing localized strings. "Dev container" and "GitHub Codespaces" already have l10n keys (used in the Quick Start webview), but this code introduces a new "Dev Container" key and returns a raw "GitHub Codespaces" string, which bypasses localization and duplicates translation work.
function executionTargetLabel(readiness: DockerReadiness): string {
    switch (readiness.executionTarget) {
        case 'wsl':
            return 'WSL';
        case 'ssh':
            return 'SSH';
        case 'devContainer':
            return l10n.t('Dev Container');
        case 'codespaces':
            return 'GitHub Codespaces';
        case 'otherRemote':

Comment thread src/tree/connections-view/LocalQuickStart/LocalQuickStartItem.ts
A connection can fail for reasons that have nothing to do with the
database: a DocumentDB Local container was stopped, a Kubernetes
port-forward tunnel died, Atlas closed the TLS handshake. The driver
reports these as ECONNREFUSED, a server-selection timeout, or an OpenSSL
alert, none of which tell the user what to do.

Add ConnectionDiagnosticsService, a registry where the source that owns
the infrastructure explains the failure in its own words. It exposes a
single method, explain(clusterId, error) -> string | undefined, and
never throws: a failing provider is skipped and a stalling one is cut
off, so the caller can always still report the original error.

Providers translate only. They never show UI, recover, or retry. One
user action can run several database commands and several can fail at
once, so a provider with side effects would produce duplicate dialogs
and errors that are obsolete by the time they appear. Anything with a
side effect stays at the call site, which alone knows whether the user
is watching.

The error itself is never modified. A lot of code here inspects errors
by identity rather than text: instanceof UserCancelledError decides
failure versus cancellation, instanceof QueryError and SettingsHintError
change handling, error.code carries server and socket codes,
errorCodeExtractor reads error.cause.cause.code at a fixed depth,
extractErrorCode parses a prefix from the start of a message, and the
tRPC boundary rebuilds errors as { code, name, message, stack, cause }.
Leaving the error alone means none of that can break.

Three providers, each identifying its own clusters differently:
- Quick Start matches an in-memory instance list, then reuses
  prepareForConnection, which also corrects a stale "Running" tree row.
- Kubernetes records clusterId while ensureReachable prepares the
  connection, then checks whether the tunnel is still up.
- Atlas checks the error shape, then the mongodb.net host suffix, and
  reuses the wording that previously existed only in the Discovery view.

Call sites are the places that render a failure: one central catch in
ConnectionsBranchDataProvider covering everything below a cluster, the
two modals in ClusterItemBase, the shell connect banner, and the query
playground. Background paths that show nothing are deliberately left
alone.

Also drop progress.md and work-summary.md, leftover scratch notes.
The connect-failure path wrote an error line and then immediately fired
the close emitter, which disposes the terminal and takes the message
with it. There is no VS Code setting to prevent that: for an
extension-owned Pseudoterminal, onDidClose always disposes, and a
non-zero exit code only adds a notification.

Show a prompt instead of closing. ShellSessionManager.evaluate() re-runs
initialize() whenever the session is uninitialized, which a failed
connect leaves it as, so the next command the user types becomes the
retry: start the container, press Enter, you are connected. A system
line says so. The setEnabled(true) call that already sat immediately
before the close now does something.

Also report the failure through two channels that outlive the terminal:
a notification, preferring the translated explanation, and an
outputChannel.error line carrying the raw driver message plus the
provider id and explanation, so a shared output channel is enough to
diagnose a report remotely.

The log line sits after extractErrorCode and before the
SettingsHintError check, so the raw message stays intact for both.
Move the diagnostics catch from ConnectionsBranchDataProvider into
BaseExtendedTreeDataProvider.wrapGetChildrenWithErrorAndStateHandling.
The Discovery, Azure Resources (vCore and RU) and Azure Workspace
providers all build on the same base method, so they now translate a
failed expansion without any per-view wiring, and a future provider gets
it for free. The Connections provider goes back to its original shape.

Behaviour is unchanged: on failure we choose what to display and rethrow
the original error object untouched, so telemetry and every downstream
identity check keep working. Cluster nodes return error children rather
than throwing, so they still handle their own failures in
ClusterItemBase.

Also record the clusterId to port-forward mapping in
KubernetesResourceItem. The Discovery view calls
ensureKubernetesPortForward directly instead of going through
ConnectionReachabilityService, so rememberKubernetesCluster never ran
for a Kubernetes connection opened from that view and
KubernetesDiagnosticsProvider stayed silent for it.
Webviews still showed the raw driver error, because the tRPC boundary
rebuilds every error as { code, name, message, stack, cause }: a class,
a custom property or an extra cause level does not survive, so an
explanation cannot ride along on the error.

Add one shared procedure, common.explainOperationFailure, which reads
the clusterId from the webview's tRPC context and returns a translated
message or null. Any webview can ask; no per-procedure result fields and
no error wrapping. Wired into the Collection view query and the Query
Insights stage errors.

Named after the caller's situation rather than a cause. The providers
behind it explain a stopped container, a dead port-forward tunnel and an
Atlas TLS rejection today, and can explain other infrastructure later
without the name becoming wrong.

Passing only the message is enough for all three providers: Quick Start
inspects container state and ignores the error, Kubernetes checks the
tunnel and then regexes the message, Atlas regexes the message and
checks the host suffix. ConnectionDiagnosticsRequest.error is already
unknown, so a string needs no signature change. The limitation is
recorded in the skill: a provider needing an error's class or code
cannot be served from a webview.
Tree-node commands (create and drop database, collection and index, and
anything else registered with
registerCommandWithTreeNodeUnwrappingAndModalErrors) reported the raw
driver error. The unwrapped node carries the cluster, so one catch in
commandErrorHandling covers all of them: on a translated failure we
suppress the default notification and show the explanation with the raw
error as detail. UserFacingError keeps its existing path, since it
already carries a deliberate message.

The Document view's three failure paths now ask
common.explainOperationFailure, the same way the Collection view does.
Its tRPC context already carries a clusterId, so no plumbing was needed.

As everywhere else, the error object is passed through untouched.
@tnaum-ms Tomasz Naumowicz (tnaum-ms) changed the title Improve Quick Start refresh and Docker status details Keep DocumentDB Local state in sync and explain infrastructure-caused failures Aug 9, 2026
Lazy hydration made a Docker discovery failure fatal: listByLabel rejects with no
docker binary or a stopped daemon, and both entry points awaited ensureHydrated()
unguarded. The tree row rendered empty behind an error toast, and the webview -
the one surface that can diagnose Docker - never opened.
…re failure

registerCommandWithTreeNodeUnwrappingAndModalErrors calls explain() for every
error that is not a UserFacingError, and both the Quick Start and Kubernetes
providers can answer without inspecting the error. Escaping a wizard on a stopped
managed instance therefore raised a modal saying DocumentDB Local was not
running. Guarded once in the service so every call site inherits it.
…ntainer

inspectContainer reports "could not ask" and "not there" identically, so
stopping Docker Desktop made the preflight assert the container had been removed
outside VS Code and recommend recreating it. The preflight now confirms the
daemon before concluding missing, and a new dockerUnreachable verdict carries
wording that matches what actually happened.
prepareForConnection() corrected state, fired the status emitter and could warn,
so the error-translation provider was repairing state and repainting the tree
while claiming to only translate. Split the verdict out as inspectManagedInstance()
and left the side effects on the connection path.
A diagnosis becomes the heading of a modal, so the four-line bulleted Atlas text
rendered as a block of bold lines above a one-line detail. The provider now
returns a single-paragraph summary; the Discovery-view modal keeps the long form
where it belongs, in the detail area.
MessageOptions.detail is only rendered for modal messages, so suppressing the
default notification and passing the driver text as detail hid it entirely.
Appended to the message instead, matching displayErrorMessage.
The connect-failure line goes to an output channel the user is encouraged to
share, and a driver error can quote the connection string, which for a Quick
Start instance carries a generated password.
Expanding the managed cluster raised a modal that blocked the expansion until it
was answered and then left the node empty, and every other non-ready verdict
rendered as silence. Each verdict now gets an actionable row, which also removes
the module-level prompt singleton that would have been shared across aliases.
Each provider had its own 5s race, so three registered sources could hold an
error back for 15s on top of the driver's own timeout.
The tooltip surfaced raw identifiers (NotInstalled, unixSocket, linux) next to
already-humanised provider and target labels. Markdown escaping is also narrowed
to the characters that change rendering, so versions and image refs read plainly.
Moving unwrapArgs out of the try meant a throw from unwrapping bypassed the
UserFacingError handling.
The context-menu refresh shells out to Docker with no feedback; the view now
carries the spinner.
…w path

Nothing in the request type stopped a future provider from reaching for
instanceof or .code and silently never matching from a webview.
The Quick Start rows drew their own `loading~spin` icons and a "· Refreshing…"
text hint instead of using `ext.state`, so progress looked and behaved
differently from every other node in the tree.

The work is service-owned — it can start from the webview, a lifecycle command,
or the background probe — so the row had nothing to await. QuickStartService now
publishes an awaitable handle for in-flight work (`getInFlightOperation`) plus a
dedicated `onDidChangeOperation` event, kept separate from `onDidChangeStatus`
because that one's listeners rebuild the whole Connections view.

A new bridge maps that handle to `ext.state.runWithTemporaryDescription` on the
instance row. It is deferred through a microtask, since work can start from
inside a tree render and applying state fires a refresh synchronously. The
lifecycle commands are deliberately NOT wrapped as well: the bridge already
covers every origin, and two owners would clip the indicator early.

The transitional rows keep their `state_*` context values — the framework
overlays description and icon but never `contextValue`, which the lifecycle
menus gate on.

Also replaces the view-level progress in the node's explicit refresh with node
progress, and drops the synthetic "busy" child row in favour of showing progress
on the node itself.

The Provisioning row keeps its own spinner: there is no instance row to attach
node progress to yet, and it mirrors what `ext.state.showCreatingChild` renders.
Starting / Stopping are only ever set inside runLifecycle and are always
superseded before it returns, so the row text was unreachable — the bridge
overlay replaces description and icon for the whole transition. Two versions of
the same sentence could only ever drift apart, so the rows now render exactly
what the overlay does.

The host is dropped from those rows: it is not actionable mid-transition and it
is still one hover away in the tooltip.

Provisioning splits the host out of its localized string as well, so the "·
localhost:" separator is no longer something translators can reorder or drop.
"Running · localhost:10260" and its Stopped twin embedded a non-translatable
host and separator in a localized string, where a translator could reorder or
drop them. The state half now comes from instanceStateLabel — the same function
the tooltip uses — so the row and the tooltip can no longer disagree about what
state the instance is in.

The remaining "·" strings are prose on both sides ("Missing · click to
recreate"), so they stay whole; splitting them would hand translators fragments
with no context.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 47 out of 47 changed files in this pull request and generated no new comments.

Suppressed comments (4)

src/tree/connections-view/LocalQuickStart/LocalQuickStartItem.ts:395

  • The background freshness path can still turn a Docker outage into a false Missing state. refreshLiveState() treats inspectContainer() === undefined as “gone” (QuickStartService.ts:1827-1843), but inspectContainer also returns undefined when Docker cannot be queried (ContainerRuntime.ts:238-245). After a hydrated node is expanded with Docker stopped, this call therefore shows the recreate guidance that the new preflight classification was intended to avoid. Please make the background refresh distinguish daemon-unreachable from confirmed absence before updating missing.
    src/utils/commandErrorHandling.ts:154
  • This does not cover the database tree-node commands claimed in the PR description. The create/drop/import/export/index commands are still registered with plain registerCommandWithTreeNodeUnwrapping in ClustersExtension.ts:1009-1067; the diagnostics-aware wrapper is only used for connection-management commands. Those database failures therefore continue to surface the default raw driver error. Please route the actual database command registrations through diagnostics-aware handling (without changing their existing modal semantics).
    src/services/connectionDiagnosticsService.ts:159
  • Racing askProviders() against a timer does not stop the provider loop. If a provider settles after 5 seconds, the loop continues querying later providers and can emit connectionDiagnostics.explained even though the caller already received undefined; this both performs post-deadline I/O and records a false “explained” event. Propagate an expiry/cancellation signal into the loop and prevent later providers and telemetry after the deadline.
        return withDeadline(this.askProviders(request));

src/documentdb/shell/DocumentDBShellPty.ts:553

  • Only initial shell connection failures use diagnostics. If the container is stopped after a shell session was established, the next command fails through handleEvalError() (DocumentDBShellPty.ts:721-750), which still prints only the raw driver error. That leaves the shell only partially covered despite the PR’s stated surface coverage. Please apply the same translation to evaluation/reconnect failures while preserving the existing error-code and SettingsHintError handling.
            const diagnosis = await ConnectionDiagnosticsService.explain({
                clusterId: this._connectionInfo.clusterId,
                error,
            });

@tnaum-ms Tomasz Naumowicz (tnaum-ms) changed the title Keep DocumentDB Local state in sync and explain infrastructure-caused failures Keep DocumentDB Local state in sync, explain failures, and unify tree progress Aug 9, 2026
UX review item 20: the managed instance row looked like a cluster but carried
none of the cluster commands, so New Database, Launch Shell and Refresh were
missing and Copy Connection String had to be duplicated as a Quick Start command.

The row deliberately does not take `treeItem_documentdbcluster`. That tag gates
thirteen entries, six of which resolve the node through connection storage —
rename, move, remove, update credentials, update connection string — and the
managed instance has no storage record for them to act on. Granting the tag and
excluding the six would mean every future cluster command reaches this row by
default, which is the wrong way round for a node that is not a stored connection.

Commands are opted in individually instead, gated to `state_running` because
every other state renders a plain row with no `cluster` to dereference. The
contribution test pins both halves: the three that must appear, and the six that
must not.

Copy Connection String and Copy Password stay as Quick Start commands. They are
also offered while the instance is stopped, where the row is not a cluster item
at all and the generic command would have no `getCredentials()` to call.
Fixes #865.

`QuickStartService` emitted user-facing text on `StageEvent.message` /
`StageEvent.error` and `QuickStartStatus.errorMessage`, so the service layer
owned copy, and every new message was one more chance to forget `l10n.t`. Five
of them had already forgotten: the stage payloads `'Checking Docker…'`,
`'Pulling the official image…'`, `'Creating container…'`, `'Starting container…'`
and `'Waiting for DocumentDB to accept connections…'` were raw English.

They were also dead. The webview labels the checklist from its own
`stageLabels()` map and only reads `message` on terminal events, so those five
strings were never rendered — untranslated text that nobody could have reported,
because nobody could see it. They are gone rather than localized.

What remains is a `QuickStartMessage`: a key, plus the data needed to phrase it
(`port`, `environment`) and a `detail` field carrying raw daemon or driver text.
`detail` is the one thing never translated, because it is evidence rather than
copy — and keeping it in its own field is what stops a daemon string being
concatenated into a sentence a translator owns.

`StageEvent.error` is gone too. It duplicated `message` at every call site
except two, where it differed only by being undefined on abort, which the
webview then fell back out of. One field and `status` say the same thing.

The wording lives in one shared `formatQuickStartMessage`, not one map per
surface. Two copies of the same sentence in the tree and the webview could only
drift, which is the failure this repo just fixed for the transitional rows.

Tests now assert keys instead of sentences, which is what #764 asks for. The M5
regression — the daemon's "Bind for …" text must not reach the user — is now
structural: a keyed message has nowhere to put it, and the test pins that
`detail` is absent rather than grepping the rendered string.
…nd detail

Two findings from the #879 review, both real.

A second readiness timeout in `resumeReadiness` fell through to
`unexpectedFailure` and put the raw `ReadinessTimeoutError` text on screen — the
one thing this PR set out to stop. It now reports `readinessTimeout` carrying the
host environment, so a repeat timeout gets the same dev-container port-routing
explanation as the first one; `stillInitializing` stays for a cancelled wait and
`unexpectedFailure` for a genuine finalize error.

`formatQuickStartMessage` trimmed `detail` and then returned it through `??`, so
a whitespace-only detail rendered as an empty message. Detail now collapses to
undefined when it carries no evidence, and `unexpectedFailure` keeps a localized
sentence around the raw text instead of replacing the copy with it — which is
what the field was documented to do.

Covered by a new test for the formatter: every key renders something, raw driver
text never stands alone, and whitespace-only detail can never blank the message.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 52 out of 52 changed files in this pull request and generated no new comments.

Suppressed comments (6)

src/webviews/documentdb/documentView/documentView.tsx:191

  • If explainOperationFailure.query() rejects, this async .catch handler will throw and suppress the user-facing error notification. The explanation query should be best-effort and never block the fallback error message.
    src/webviews/documentdb/documentView/documentView.tsx:242
  • If explainOperationFailure.query() rejects, this async .catch handler will throw and the save failure may not be surfaced. The explanation query should be best-effort and fall back to the original localized message.
    src/webviews/documentdb/collectionView/queryInsightsTab/QueryInsightsTab.tsx:264
  • The new explainOperationFailure call is not failure-tolerant: if the tRPC query rejects (e.g., extension host busy/reloading), the .then() chain is skipped and no user-visible error message is shown. This is a regression from the previous always-show behavior.
    src/webviews/documentdb/documentView/documentView.tsx:86
  • If explainOperationFailure.query() rejects, this async .catch handler will throw and no error message will be displayed to the user. The error-explanation step should be best-effort and fall back to the original localized message.

This issue also appears in the following locations of the same file:

  • line 187
  • line 238
    src/webviews/documentdb/collectionView/CollectionView.tsx:408
  • If explainOperationFailure.query() rejects, this async .catch handler will throw and the query failure may not be shown to the user. The explanation step should be best-effort and fall back to the original localized message.
    l10n/bundle.l10n.json:1299
  • This string has corrupted apostrophes ("machineȁs", "projectȁs"), which will show up incorrectly in the UI. It should use the intended right single quotation mark (’) or a plain apostrophe.
  "MongoDB Atlas closed the TLS connection with an internal error. That is a transport-level rejection rather than a failed sign-in, so it is worth checking whether this machineȁs IP address is on the projectȁs IP access list, and whether the cluster is paused.": "MongoDB Atlas closed the TLS connection with an internal error. That is a transport-level rejection rather than a failed sign-in, so it is worth checking whether this machineȁs IP address is on the projectȁs IP access list, and whether the cluster is paused.",

…overage

All four were real, and three of them were places where this PR claimed a
surface it had not actually reached.

**A Docker outage still read as a deleted container.** The preflight learned to
tell "could not ask" from "not there" (`classifyUninspectableContainer`), but the
background freshness probe kept its old logic and set `missing` on any empty
inspect. Stopping Docker Desktop under an expanded node therefore offered to
recreate a container that was sitting on disk, untouched — the exact wrong turn
the preflight work existed to prevent. `refreshLiveState()` now confirms the
daemon is answering before concluding anything, and keeps the last known state
otherwise.

**The database commands were never wired up.** The description claimed tree-node
commands were covered; only the connection-management ones were. Rather than
reuse the modal wrapper, which would have turned every failed drop or import into
a blocking dialog, this adds
`registerCommandWithTreeNodeUnwrappingAndDiagnostics`: same translation, reported
the way those commands already report failures. Nine registrations move over.

**The explain deadline did not stop anything.** It was a `Promise.race`, so the
losing side kept going: later providers were still queried, and a provider that
answered after the caller had been handed `undefined` still recorded
`connectionDiagnostics.explained`. The deadline is now an `AbortSignal` the loop
checks before each provider and again after each await.

**The shell was only half covered.** A session that connects fine can still break
underneath the user — the container stops, a port-forward drops — and the next
command is where they find out. That path printed the raw driver error.
`handleEvalError` now asks for a diagnosis too, keeping the existing error-code
and SettingsHintError handling intact.

Also localizes the tooltip's product labels, which were raw strings while the
webview localizes the same words, and drops a duplicate "Dev Container" key that
differed from the webview's "Dev container" only by case.
@tnaum-ms

Copy link
Copy Markdown
Collaborator Author

Replying to the four suppressed comments in review #4892351921 — they have no threads of their own, so they get one comment here. All four were correct, and three of them were places where this PR's description claimed a surface it had not actually reached. Fixed in 0194ac26.


1. LocalQuickStartItem.ts:395 — a Docker outage still read as a deleted container · fixed

The sharpest of the four, because the PR added the machinery to prevent exactly this and then only used it on one path.

prepareForConnection() learned to tell "could not ask" from "not there" via classifyUninspectableContainer(). refreshLiveState() kept the old logic:

const inspected = await this.runtime.inspectContainer(containerId);
if (!inspected) {
    entry.missing = true;   // no idea whether Docker was even answering

Since ContainerRuntime.inspectContainer swallows every failure into undefined, stopping Docker Desktop under an expanded node flipped a healthy instance to Missing and offered to recreate a container that was sitting on disk, untouched. The background probe now confirms the daemon is answering before concluding anything, and otherwise keeps the last known state rather than guessing.

Regression test: refreshLiveState() does not report Missing when Docker cannot be asked. The existing "TRANSITION into Missing" test still passes, which matters — the fix must not stop us detecting a genuinely removed container.

2. commandErrorHandling.ts:154 — database commands were never wired up · fixed, differently than suggested

Correct: only connection-management commands used the diagnostics-aware wrapper. createDatabase, dropDatabase, dropCollection, createCollection, importDocuments, exportDocuments, hideIndex, unhideIndex and dropIndex were all on plain registerCommandWithTreeNodeUnwrapping, so the description overstated the coverage.

I did not move them onto registerCommandWithTreeNodeUnwrappingAndModalErrors, because that wrapper also promotes every UserFacingError to a blocking modal — which would have changed the failure UX of nine commands well beyond diagnostics, and you explicitly asked for the modal semantics to stay put.

Instead there is a new registerCommandWithTreeNodeUnwrappingAndDiagnostics: identical translation step, UserFacingError left to default handling, and the diagnosis reported as a notification with the raw cause appended — the same shape BaseExtendedTreeDataProvider already uses. Nine registrations moved over.

Also confirming your nuance, since it is worth having on record: fetchChildrenWithDiagnostics covers tree expansion only. A failed drop-collection never passes through it, which is precisely why the gap existed.

3. connectionDiagnosticsService.ts:159 — the deadline did not stop anything · fixed

Correct, and the false-telemetry half is the part that would have quietly poisoned the data. Promise.race abandons the losing promise, it does not cancel it, so the loop kept querying later providers and a late answer still recorded connectionDiagnostics.explained for a caller that had already been handed undefined.

The deadline is now an AbortSignal the loop checks before each provider and again after each await — the second check is the one that matters, since that is the window in which the deadline actually elapses. withDeadline() is gone.

New test: stops querying providers once the deadline has passed — asserts the next provider is never asked and no telemetry is emitted after the caller has given up.

4. DocumentDBShellPty.ts:553 — the shell was only half covered · fixed

Correct, and this one is close to the issue that started the PR: a session that connects fine can still break underneath the user — the container stops, a port-forward drops — and the next command is where they find out. That path went through handleEvalError() and printed the raw driver error.

It now asks for a diagnosis too, on a separate line so the raw text above stays intact for extractErrorCode. Both things you flagged as needing to survive do: the SettingsHintError hint line and the error-code-50 .maxTimeMS() tip are untouched and still run after.

One consequence worth naming rather than burying: handleEvalError had to become async, so a failed command can now wait on a diagnosis before the prompt returns. Bounded by the 5s deadline, and in practice a provider that does not recognise the cluster returns without I/O — but it is a real change to shell timing, and if it turns out to be felt, the answer is to print the raw error first and append the diagnosis, not to revert the coverage.


Verification: npm run l10n, npm run prettier-fix, npm run lint clean, npx jest --no-coverage 218 suites / 3,499 tests, npm run build clean.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Code Quality Checks

Check Status How to fix
Localization (l10n) ✅ Passed
ESLint ✅ Passed
Prettier formatting ✅ Passed

This comment is updated automatically on each push.

@github-actions

Copy link
Copy Markdown
Contributor

📦 Build Size Report

Metric Base (release/0.10.0) PR Delta
VSIX (vscode-documentdb-0.10.0-bug-bash-2.vsix) 8.01 MB 8.15 MB ⬆️ +141 KB (+1.7%)
Webview bundle (views.js) 5.88 MB 6.11 MB ⬆️ +233 KB (+3.9%)

Download artifact · updated automatically on each push.

@tnaum-ms
Tomasz Naumowicz (tnaum-ms) merged commit a0709cf into release/0.10.0 Aug 10, 2026
5 checks passed
@tnaum-ms
Tomasz Naumowicz (tnaum-ms) deleted the dev/tnaum/quickstart-improvements branch August 10, 2026 08:37
@tnaum-ms Tomasz Naumowicz (tnaum-ms) added this to the 0.10.0 milestone Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

2 participants